package com.ervanalb.cellradio.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.SocketException;

import com.ervanalb.cellradio.shared.ClientState;
import com.ervanalb.cellradio.shared.communicator.Communicator;
import com.ervanalb.cellradio.shared.communicator.QueuedListener;
import com.ervanalb.cellradio.shared.communicator.ResponseListener;
import com.ervanalb.cellradio.shared.communicator.SocketSink;
import com.ervanalb.cellradio.shared.communicator.packets.AuthenticatePacketData;
import com.ervanalb.cellradio.shared.communicator.packets.ConnectPacketData;
import com.ervanalb.cellradio.shared.communicator.packets.DisconnectPacketData;
import com.ervanalb.cellradio.shared.communicator.packets.ErrorPacketData;
import com.ervanalb.cellradio.shared.communicator.packets.HelloPacketData;
import com.ervanalb.cellradio.shared.communicator.packets.Packet;
import com.ervanalb.cellradio.shared.communicator.packets.PacketData;

/**
 * The main client loop. Instantiating this class creates a new thread, which
 * handles communication with the server. It may be issued asynchronous commands
 * to control it. Incoming commands are processed in run().
 * 
 * @author eric
 */
public class ClientMain extends Thread {
	public Communicator comm;
	public InetAddress serverAddr;
	public QueuedListener listener;
	public int id;
	protected boolean go;
	public ClientState cs;

	public static int CONNECT_TIMEOUT = 100;
	public static int AUTHENTICATE_TIMEOUT = 100;

	public ClientMain(int listenPort, int sendPort, InetAddress serverAddr)
			throws SocketException {

		this.serverAddr = serverAddr;

		listener = new QueuedListener();

		cs = new ClientState();

		comm = new Communicator(new SocketSource(4002), new SocketSink(4003),
				listener);
		start();
	}

	public void run() {
		Packet p;

		go = true;

		while (go) {
			try {
				p = listener.pop(100);
				if (p == null) {
					continue;
				}

				if (p.channel != id) {
					sendError("Bad machine");
					continue;
				}

				else if (p.pd.type == PacketData.TYPE_ERROR) {
					System.out
							.println("Got error:" + p.pd.parseError().message);
					disconnect();
				}

				else if (p.pd.type == PacketData.TYPE_DISCONNECT) {
					System.out.println("Client was told to disconnect.");
					cutoff();
				}

			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		comm.finish();
	}

	public void finish() {
		go = false;
		interrupt();
	}

	public Informational<Boolean> tellServer(PacketData pd) throws IOException {
		return tellServer(pd, 0);
	}

	public Informational<Boolean> tellServer(PacketData pd, int replyTo)
			throws IOException {
		Packet p = new Packet(0, id, replyTo, pd, serverAddr);

		Informational<ResponseListener> i = comm.safeSend(p, false);
		if (i != null) // Completely null result means success
			return new Informational<Boolean>(false, "Failed to tell server: "
					+ i.message);

		return new Informational<Boolean>(true);
	}

	public Informational<ResponseListener> queryServer(PacketData pd)
			throws IOException {
		Packet p = new Packet(0, id, 0, pd, serverAddr);
		Informational<ResponseListener> i = comm.safeSend(p, true);
		if (i.result == null)
			i.message = "Failed to query server: " + i.message;
		return i;
	}

	public Informational<Packet> queryServerBlock(PacketData pd, long timeout)
			throws IOException {

		Packet p = new Packet(0, id, 0, pd, serverAddr);
		Informational<ResponseListener> i = comm.safeSend(p, true);

		ResponseListener rl = i.result;
		if (rl == null) {
			return new Informational<Packet>("Failed to query server: "
					+ i.message);
		}

		Packet response = rl.getReply(timeout);
		if (response == null)
			return new Informational<Packet>(
					"Timed out waiting for reply from server.");
		return new Informational<Packet>(response);
	}

	public Informational<Boolean> connect() throws IOException {
		int newId;

		Informational<Packet> i = queryServerBlock(
				new ConnectPacketData().pack(), CONNECT_TIMEOUT);

		Packet connected = i.result;
		if (connected == null)
			return new Informational<Boolean>(false, "Failed to connect:"
					+ i.message);

		if (connected.pd.type == PacketData.TYPE_CONNECTED) {
			newId = connected.pd.parseConnected().id;
			if (newId == 0) {
				return new Informational<Boolean>(false,
						"Failed to connect: Server refused request");
			} else {
				id = newId;
				comm.reset();
				Informational<Boolean> i2 = hello();
				if (!i2.result)
					return i2;

				cs.connected.set(true);
				return i2;
			}
		} else if (connected.pd.type == PacketData.TYPE_ERROR) {
			return new Informational<Boolean>(false,
					"Failed to connect: Got error from server: "
							+ connected.pd.parseError().message);
		} else {
			return new Informational<Boolean>(false,
					"Failed to connect: Unexpected reply from server.");
		}
	}

	public Informational<Boolean> hello() throws IOException {
		Informational<Boolean> i = sendHello();
		if (!i.result) {
			disconnect();
		}
		return i;
	}

	public void disconnect() throws IOException {
		if (cs.connected.get()) {
			sendDisconnect();
		}
		cutoff();
	}

	public void cutoff() {
		id = 0;
		comm.reset();
		cs.connected.set(false);
	}

	public Informational<Boolean> sendHello() throws IOException {
		PacketData pd = new HelloPacketData().pack();
		Informational<Boolean> i = tellServer(pd);
		if (!i.result)
			return new Informational<Boolean>(false, "Send hello: " + i.message);
		return i;
	}

	public Informational<Boolean> sendDisconnect() throws IOException {
		PacketData pd = new DisconnectPacketData().pack();
		Informational<Boolean> i = tellServer(pd);
		if (!i.result)
			return new Informational<Boolean>(false, "Send disconnect: "
					+ i.message);
		return i;
	}

	public Informational<Boolean> sendError(String error) throws IOException {
		PacketData pd = new ErrorPacketData(error).pack();
		Informational<Boolean> i = tellServer(pd);
		if (!i.result)
			return new Informational<Boolean>(false, "Send error: " + i.message);
		return i;
	}

	public Informational<Boolean> authenticate(String username)
			throws IOException {
		Informational<Packet> i = queryServerBlock(new AuthenticatePacketData(
				username).pack(), AUTHENTICATE_TIMEOUT);

		Packet connected = i.result;
		if (connected == null)
			return new Informational<Boolean>(false, "Failed to authenticate:"
					+ i.message);

		if (connected.pd.type == PacketData.TYPE_ERROR) {
			return new Informational<Boolean>(false,
					"Failed to authenticate: Got error from server: "
							+ connected.pd.parseError().message);
		}
		if (connected.pd.type != PacketData.TYPE_AUTHENTICATED) {
			return new Informational<Boolean>(false,
					"Failed to authenticate: Unexpected reply from server.");
		}
		if (connected.pd.parseAuthenticated().success == 0) {
			return new Informational<Boolean>(false,
					"Failed to authenticate: Server refused request.");
		}

		cs.username.set(username);
		cs.authenticated.set(true);
		return new Informational<Boolean>(true);
	}

	public static void main(String[] arg) throws InterruptedException,
			IOException {
		ClientMain c = new ClientMain(4003, 4004, InetAddress.getLocalHost());

		System.out.println("Client running.");
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String str;
		for (;;) {
			str = br.readLine();
			if (str.equals("quit")) {
				c.finish();
				break;
			} else if (str.equals("connect")) {
				System.out.println(c.connect());
			} else if (str.equals("hello")) {
				System.out.println(c.hello());
			} else if (str.equals("authenticate")) {
				System.out.println(c.authenticate("eric"));
			} else if (str.equals("disconnect")) {
				c.disconnect();
				System.out.println("Disconnected.");
			} else {
				System.out.println("Unrecognized command.");
			}
		}
		System.out.println("Goodbye.");
	}
}
